from util.bound_numbers import bound
from util.matrix import Matrix

__author__ = 'Douglas'


class BackpropagationLayer:
    def __init__(self, backpropagation, layer):
        self.backpropagation = backpropagation
        self.layer = layer
        self.error = [0.0] * self.layer.neuron_count
        self.error_delta = self.error[:]

        if self.layer.next is not None:
            self.acc_matrix_delta = Matrix(self.layer.neuron_count + 1, layer.next.neuron_count)
            self.matrix_delta = Matrix(self.acc_matrix_delta)
            self.bias_row = self.layer.neuron_count
        else:
            self.acc_matrix_delta = None
            self.matrix_delta = None
            self.bias_row = 0

    def calculate_error(self, ideal_output=None):
        if ideal_output is None:
            next_layer = self.backpropagation.layer_map[self.layer.next]

            for i in range(self.layer.next.neuron_count):
                for j in range(self.layer.neuron_count):
                    self.accumulate_matrix_delta(j, i, next_layer.error_delta[i] * self.layer.fire[j])
                    self.error[j] = bound(self.error[j] + self.layer.matrix[j][i] * next_layer.error_delta[i])

                self.accumulate_threshold_delta(i, next_layer.error_delta[i])

            if self.layer.is_hidden:
                self.error_delta = [bound(self.calculate_delta(i)) for i in range(self.layer.neuron_count)]
        else:
            for i in range(self.layer.neuron_count):
                self.error[i] = bound(ideal_output[i] - self.layer.fire[i])
                self.error_delta[i] = bound(self.calculate_delta(i))

    def accumulate_matrix_delta(self, row, column, value):
        self.acc_matrix_delta[row][column] += value

    def accumulate_threshold_delta(self, index, value):
        self.acc_matrix_delta[self.bias_row][index] += value

    def calculate_delta(self, i):
        return self.error[i] * self.layer.activation_function.derivative_function(self.layer.fire[i])

    def clear_error(self):
        self.error = [0.0] * self.layer.neuron_count

    def learn(self, learn_rate, momentum):
        if self.layer.matrix is not None:
            m1 = self.acc_matrix_delta * learn_rate
            m2 = self.matrix_delta * momentum
            self.matrix_delta = m1 + m2
            self.layer.matrix += self.matrix_delta
            self.acc_matrix_delta.clear()